Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit f640f074d85121887a02d1db4863d6ca1cdd1659


Parents : 4114df7
Author : Mark Qvist <bc7291552be7a58f361522990465165c>
Date : 2026-05-19T23:20:19+02:00

Added markdown rendering and compose support to messages

Changes
Diff

diff --git a/nomadnet/Conversation.py b/nomadnet/Conversation.py
index d7039dc..26a5089 100644
--- a/nomadnet/Conversation.py
+++ b/nomadnet/Conversation.py
@@ -417,6 +417,7 @@ class ConversationMessage:
self.timestamp = self.lxm.timestamp
self.sort_timestamp = os.path.getmtime(self.file_path)
+
if self.lxm.state > LXMF.LXMessage.GENERATING and self.lxm.state < LXMF.LXMessage.SENT:
found = False
@@ -597,6 +598,18 @@ class ConversationMessage:
return {}
+ def content_renderer(self):
+ if not self.loaded: self.load()
+ RNS.log(self.lxm)
+ if self.lxm and hasattr(self.lxm, "get_fields"):
+ fields = self.lxm.get_fields()
+ RNS.log(f"has fields: {fields}")
+ if fields and isinstance(fields, dict):
+ if LXMF.FIELD_RENDERER in fields:
+ return fields[LXMF.FIELD_RENDERER]
+
+ return None
+
def has_attachments(self):
if self._cached_has_attachments is not None:
return self._cached_has_attachments

diff --git a/nomadnet/NomadNetworkApp.py b/nomadnet/NomadNetworkApp.py
index f3afd73..e578bbd 100644
--- a/nomadnet/NomadNetworkApp.py
+++ b/nomadnet/NomadNetworkApp.py
@@ -138,6 +138,7 @@ class NomadNetworkApp:
self.try_propagation_on_fail = True
self.disable_propagation = True
self.notify_on_new_message = True
+ self.compose_markdown = True
self.lxmf_max_propagation_size = None
self.lxmf_max_sync_size = None
@@ -836,6 +837,10 @@ class NomadNetworkApp:
value = self.config["client"].as_bool(option)
self.notify_on_new_message = value
+ if option == "compose_in_markdown":
+ value = self.config["client"].as_bool(option)
+ self.compose_markdown = value
+
if option == "user_interface":
value = value.lower()
if value == "none":
@@ -1146,7 +1151,6 @@ downloads_path = ~/Downloads
# Where to save received attachments. If not set,
# attachments will be saved to the downloads path.
# attachment_save_path = ~/Downloads
-
notify_on_new_message = yes
# By default, the peer is announced at startup
@@ -1210,6 +1214,10 @@ max_accepted_size = 500
# been received, for every destination.
compact_announce_stream = yes
+# You can choose whether or not to compose
+# messages in markdown format.
+compose_in_markdown = yes
+
[textui]
# Amount of time to show intro screen

diff --git a/nomadnet/ui/TextUI.py b/nomadnet/ui/TextUI.py
index f05ae81..3cf4a3c 100644
--- a/nomadnet/ui/TextUI.py
+++ b/nomadnet/ui/TextUI.py
@@ -5,6 +5,7 @@ import os
import platform
import nomadnet
+from nomadnet.ui import THEME_DARK, THEME_LIGHT
from nomadnet.ui.textui import *
from nomadnet import NomadNetworkApp
@@ -13,8 +14,6 @@ COLORMODE_16 = 16
COLORMODE_88 = 88
COLORMODE_256 = 256
COLORMODE_TRUE = 2**24
-THEME_DARK = 0x01
-THEME_LIGHT = 0x02
THEMES = {
THEME_DARK: {

diff --git a/nomadnet/ui/__init__.py b/nomadnet/ui/__init__.py
index b11aadb..cb06cf8 100644
--- a/nomadnet/ui/__init__.py
+++ b/nomadnet/ui/__init__.py
@@ -8,6 +8,8 @@ pyc_modules = glob.glob(os.path.dirname(__file__)+"/*.pyc")
modules = py_modules+pyc_modules
__all__ = list(set([os.path.basename(f).replace(".pyc", "").replace(".py", "") for f in modules if not (f.endswith("__init__.py") or f.endswith("__init__.pyc"))]))
+THEME_DARK = 0x01
+THEME_LIGHT = 0x02
UI_NONE = 0x00
UI_MENU = 0x01

diff --git a/nomadnet/ui/textui/Channels.py b/nomadnet/ui/textui/Channels.py
index ea119d8..be26dee 100644
--- a/nomadnet/ui/textui/Channels.py
+++ b/nomadnet/ui/textui/Channels.py
@@ -13,10 +13,11 @@ from nomadnet.ui.textui.MicronParser import LinkableText, LinkSpec
from RNS.Utilities.rngit.util import MarkdownToMicron
from RNS.Utilities.rngit.highlight import SyntaxHighlighter
from .MicronParser import markup_to_attrmaps
-from nomadnet.util import strip_modifiers
+from nomadnet.util import strip_modifiers, strip_micron, strip_escaped_micron, unescape_micron, strip_non_formatting_tags
-theme_dark = { "ts": "888",
+theme_dark = { "text": "ddd",
+ "ts": "888",
"nick_self": "6c5",
"nick_peer": "3cd",
"notice": "fd3",
@@ -25,7 +26,8 @@ theme_dark = { "ts": "888",
"mention": "fb4",
"link": "79d", }
-theme_light = { "ts": "888",
+theme_light = { "text": "111",
+ "ts": "888",
"nick_self": "3a0",
"nick_peer": "077",
"notice": "a70",
@@ -899,47 +901,6 @@ class _ChatLinkDelegate:
except Exception as e:
RNS.log("Could not open page link: "+str(e), RNS.LOG_ERROR)
-def strip_micron(text):
- text = re.sub(r'`[FB][0-9a-fA-F]{3}', '', text)
- text = re.sub(r'`[FB]T[0-9a-fA-F]{6}', '', text)
- text = re.sub(r'`[!*_=]', '', text)
- text = re.sub(r'`f`b', '', text)
- text = re.sub(r'`f', '', text)
- text = re.sub(r'`b', '', text)
- text = re.sub(r'`<', '', text)
- text = re.sub(r'`>', '', text)
- text = re.sub(r'`{', '', text)
- return text
-
-def strip_escaped_micron(text):
- text = re.sub(r'¦[FB][0-9a-fA-F]{3}', '', text)
- text = re.sub(r'¦[FB]T[0-9a-fA-F]{6}', '', text)
- text = re.sub(r'¦[!*_=]', '', text)
- text = re.sub(r'¦f`b', '', text)
- text = re.sub(r'¦f', '', text)
- text = re.sub(r'¦b', '', text)
- text = re.sub(r'¦<', '', text)
- text = re.sub(r'¦>', '', text)
- text = re.sub(r'¦{', '', text)
- return text
-
-def unescape_micron(text):
- text = re.sub(r'¦([FB][0-9a-fA-F]{3})', r'`\1', text)
- text = re.sub(r'¦([FB]T[0-9a-fA-F]{6})', r'`\1', text)
- text = re.sub(r'¦([!*_=])', r'`\1', text)
- text = re.sub(r'¦(f`b)', r'`\1', text)
- text = re.sub(r'¦(f)', r'`\1', text)
- text = re.sub(r'¦(b)', r'`\1', text)
- text = re.sub(r'¦(<)', r'`\1', text)
- text = re.sub(r'¦(>)', r'`\1', text)
- text = re.sub(r'¦({)', r'`\1', text)
- return text
-
-def strip_non_formatting_tags(text):
- text = re.sub(r'`<', '', text)
- text = re.sub(r'`>', '', text)
- text = re.sub(r'`{', '', text)
- return text
mdc = MarkdownToMicron(max_width=80, syntax_highlighter=SyntaxHighlighter(), url_scope=None)
def _message_widget(app, hub, m, link_delegate=None):
@@ -1023,22 +984,22 @@ def _message_widget(app, hub, m, link_delegate=None):
prefix_micron = f"{irc_ts}{_ts_prefix_raw(m.ts)}"
nick_micron = f"`f{nick_attr}<{sender}>`f "
if app.rrc_ui_justify_msgs:
- prefix_rendered = _render_body(f"{prefix_micron}")
- body_rendered = _render_body(f"{nick_micron}{message_body}")
+ prefix_rendered = _render_body(f"{prefix_micron}", fg=t["text"])
+ body_rendered = _render_body(f"{nick_micron}{message_body}", fg=t["text"])
if app.rrc_ui_space_msgs: body_rendered.append(urwid.Text(""))
columns = urwid.Columns([(urwid.PACK, urwid.Pile(prefix_rendered)), urwid.Pile(body_rendered)], dividechars=1)
final_widget = urwid.Padding(columns, left=1)
else:
- rendered = _render_body(f"{prefix_micron}{nick_micron}{message_body}", link_delegate=ld)
+ rendered = _render_body(f"{prefix_micron}{nick_micron}{message_body}", link_delegate=ld, fg=t["text"])
if app.rrc_ui_space_msgs: rendered.append(urwid.Text(""))
final_widget = urwid.Padding(urwid.Pile(rendered), left=1)
final_widget.msg = m
return final_widget
-def _render_body(markup, link_delegate=None):
- try: return markup_to_attrmaps(strip_modifiers(markup), url_delegate=link_delegate, link_class=_ChatLinkableText)
+def _render_body(markup, link_delegate=None, fg="bbb"):
+ try: return markup_to_attrmaps(strip_modifiers(markup), url_delegate=link_delegate, link_class=_ChatLinkableText, fg_color=fg)
except Exception as e:
RNS.trace_exception(e)
return []

diff --git a/nomadnet/ui/textui/Conversations.py b/nomadnet/ui/textui/Conversations.py
index 57521e3..872b49b 100644
--- a/nomadnet/ui/textui/Conversations.py
+++ b/nomadnet/ui/textui/Conversations.py
@@ -15,6 +15,11 @@ from nomadnet.Conversation import ConversationMessage
from nomadnet.util import strip_modifiers
from nomadnet.util import sanitize_name
+from RNS.Utilities.rngit.util import MarkdownToMicron
+from RNS.Utilities.rngit.highlight import SyntaxHighlighter
+from .MicronParser import markup_to_attrmaps
+from nomadnet.util import strip_modifiers, strip_micron, strip_escaped_micron, unescape_micron, strip_non_formatting_tags
+from nomadnet.ui import THEME_DARK, THEME_LIGHT
def relative_time(timestamp):
now = time.time()
@@ -1302,7 +1307,7 @@ class ConversationWidget(urwid.WidgetWrap):
if not message_hash in added_hashes:
added_hashes.append(message_hash)
was_loaded = message.loaded
- message_widget = LXMessageWidget(message)
+ message_widget = LXMessageWidget(message, theme=self.app.config["textui"]["theme"])
self.message_widgets.append(message_widget)
if not was_loaded and message.loaded:
needs_index.append(message)
@@ -1467,6 +1472,10 @@ class ConversationWidget(urwid.WidgetWrap):
if file_attachments:
fields = {LXMF.FIELD_FILE_ATTACHMENTS: file_attachments}
+ if self.app.compose_markdown:
+ if not fields: fields = {}
+ fields[LXMF.FIELD_RENDERER] = LXMF.RENDERER_MARKDOWN
+
if self.conversation.send(content, title, fields=fields):
self.clear_editor()
@@ -1609,7 +1618,9 @@ class ConversationWidget(urwid.WidgetWrap):
class LXMessageWidget(urwid.WidgetWrap):
- def __init__(self, message):
+ mdc = MarkdownToMicron(max_width=80, syntax_highlighter=SyntaxHighlighter(), url_scope=None)
+
+ def __init__(self, message, theme=THEME_DARK):
app = nomadnet.NomadNetworkApp.get_shared_instance()
g = app.ui.glyphs
self.timestamp = message.get_timestamp()
@@ -1623,6 +1634,7 @@ class LXMessageWidget(urwid.WidgetWrap):
msg_method = message._cached_method
time_format = app.time_format
message_time = datetime.fromtimestamp(self.timestamp)
+ renderer = message.content_renderer()
encryption_string = ""
if message.get_transport_encrypted():
encryption_string = " "+g["encrypted"]
@@ -1685,7 +1697,16 @@ class LXMessageWidget(urwid.WidgetWrap):
content_text = message.get_content()
content_lines = content_text.split("\n")
- indented = "\n".join(" "+line for line in content_lines)
+ markdown = renderer == LXMF.RENDERER_MARKDOWN
+
+ default_fg = "bbb" if theme == THEME_DARK else "444"
+ if markdown:
+ formatted = self.mdc.format_block(content_text)
+ message_body = strip_non_formatting_tags(formatted)
+ rendered = markup_to_attrmaps(strip_modifiers(message_body), url_delegate=None, fg_color=default_fg, bg_color=None)
+ content_pile = urwid.Padding(urwid.Pile(rendered), left=2, right=2)
+
+ else: indented = "\n".join(" "+line for line in content_lines)
pile_widgets = [title]
@@ -1711,7 +1732,8 @@ class LXMessageWidget(urwid.WidgetWrap):
pile_widgets.append(self.progress_attr)
self._start_progress_poll()
- pile_widgets.append(urwid.Text(indented))
+ if markdown: pile_widgets.append(content_pile)
+ else: pile_widgets.append(urwid.Text(indented))
if has_attachments and cached_names:
att_file_idx = 0

diff --git a/nomadnet/ui/textui/Guide.py b/nomadnet/ui/textui/Guide.py
index 840dcc8..f80e870 100644
--- a/nomadnet/ui/textui/Guide.py
+++ b/nomadnet/ui/textui/Guide.py
@@ -771,6 +771,12 @@ The maximum accepted unpacked size for messages received directly from other pee
With this option enabled, Nomad Network will only display one entry in the announce stream per destination. Older announces are culled when a new one arrives.
<
+>>>
+`!compose_in_markdown = yes`!
+>>>>
+Configures whether sent messages are composed in markdown format.
+<
+
>> RRC Section
This section hold configuration directives related to the RRC client behaviour. It is delimited by the `![rrc]`! header in the configuration file. Available directives, along with their default values, are as follows:

diff --git a/nomadnet/util.py b/nomadnet/util.py
index 9f2562a..9b3427c 100644
--- a/nomadnet/util.py
+++ b/nomadnet/util.py
@@ -141,3 +141,45 @@ def sanitize_name(name):
name = name.strip()
return name
+
+def strip_micron(text):
+ text = re.sub(r'`[FB][0-9a-fA-F]{3}', '', text)
+ text = re.sub(r'`[FB]T[0-9a-fA-F]{6}', '', text)
+ text = re.sub(r'`[!*_=]', '', text)
+ text = re.sub(r'`f`b', '', text)
+ text = re.sub(r'`f', '', text)
+ text = re.sub(r'`b', '', text)
+ text = re.sub(r'`<', '', text)
+ text = re.sub(r'`>', '', text)
+ text = re.sub(r'`{', '', text)
+ return text
+
+def strip_escaped_micron(text):
+ text = re.sub(r'¦[FB][0-9a-fA-F]{3}', '', text)
+ text = re.sub(r'¦[FB]T[0-9a-fA-F]{6}', '', text)
+ text = re.sub(r'¦[!*_=]', '', text)
+ text = re.sub(r'¦f`b', '', text)
+ text = re.sub(r'¦f', '', text)
+ text = re.sub(r'¦b', '', text)
+ text = re.sub(r'¦<', '', text)
+ text = re.sub(r'¦>', '', text)
+ text = re.sub(r'¦{', '', text)
+ return text
+
+def unescape_micron(text):
+ text = re.sub(r'¦([FB][0-9a-fA-F]{3})', r'`\1', text)
+ text = re.sub(r'¦([FB]T[0-9a-fA-F]{6})', r'`\1', text)
+ text = re.sub(r'¦([!*_=])', r'`\1', text)
+ text = re.sub(r'¦(f`b)', r'`\1', text)
+ text = re.sub(r'¦(f)', r'`\1', text)
+ text = re.sub(r'¦(b)', r'`\1', text)
+ text = re.sub(r'¦(<)', r'`\1', text)
+ text = re.sub(r'¦(>)', r'`\1', text)
+ text = re.sub(r'¦({)', r'`\1', text)
+ return text
+
+def strip_non_formatting_tags(text):
+ text = re.sub(r'`<', '', text)
+ text = re.sub(r'`>', '', text)
+ text = re.sub(r'`{', '', text)
+ return text
\ No newline at end of file


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────